package com.deng.eventflow.core;

import com.deng.eventflow.core.exception.IncompatibleTypesException;
import com.deng.eventflow.core.exception.IndexOutOfBoundsException;
import com.deng.eventflow.core.exception.SelfLoopException;
import com.deng.eventflow.core.exception.Variant;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

public class Connector {

    public static final boolean s_checkForTypes = true;

    public static final boolean s_checkForBounds = false;

    public static final int INPUT = 0;

    public static final int OUTPUT = 0;

    public static final int LEFT = 0;

    public static final int TOP = 0;

    public static final int RIGHT = 1;

    public static final int BOTTOM = 1;

    private Connector() {
        throw new IllegalAccessError("Utility class");
    }

    public static Processor connect(Processor p1, int i, Processor p2, int j) {
        return connect(null, p1, i, p2, j);
    }

    public static Processor connect(Processor... procs) {
        return connect(null, procs);
    }


    public static Processor connect(EventTracker tracker, Processor ... procs) {
        if (procs.length == 1)
        {
            // If given only one processor, do nothing
            return procs[0];
        }
        for (int j = 0; j < procs.length - 1; j++)
        {
            Processor p1 = procs[j];
            Processor p2 = procs[j + 1];
            int arity = p1.getOutputArity();
            for (int i = 0; i < arity; i++)
            {
                connect(tracker, p1, i, p2, i);
            }
        }
        return procs[procs.length - 1];
    }

    public static Processor connect(EventTracker tracker, Processor p1, int i, Processor p2, int j) {
        // First check for type compatibility
        if (s_checkForTypes)
        {
            // This will throw an exception if the connection is impossible
            checkForException(p1, i, p2, j);
        }
        if (p1 == p2)
        {
            // This is weird: you try to connect a processor to itself
            throw new SelfLoopException(p1, i, p2, j);
        }
        // Pull
        try
        {
            Pullable p1_out = p1.getPullableOutput(i);
            p2.setPullableInput(j, p1_out);
        }
        catch (ArrayIndexOutOfBoundsException e)
        {
            throw new IndexOutOfBoundsException(p1, i, p2, j);
        }
        catch (UnsupportedOperationException e)
        {
            // It's OK. Some processors deliberately throw this
            // exception to warn an end user that they don't have a pushable
            // or a pullable, but the connector does not care.
        }
        // Push
        try
        {
            Pushable p2_in = p2.getPushableInput(j);
            p1.setPushableOutput(i, p2_in);
        }
        catch (ArrayIndexOutOfBoundsException e)
        {
            throw new IndexOutOfBoundsException(p1, i, p2, j);
        }
        catch (UnsupportedOperationException e)
        {
            // Same as above
        }
        if (tracker != null)
        {
            tracker.setConnection(p1.getId(), i, p2.getId(), j);
        }
        return p2;
    }

    public static class Connection
    {
        /**
         * The ID of the source processor
         */
        protected int m_sourceProcessorId;

        /**
         * The number of the source pipe
         */
        protected int m_sourcePipeNumber;

        /**
         * The ID of the destination processor
         */
        protected int m_destinationProcessorId;

        /**
         * The number of the destination pipe
         */
        protected int m_destinationPipeNumber;

        /**
         * Empty constructor
         */
        protected Connection()
        {
            this(-1, -1, -1, -1);
        }

        /**
         * Creates a new connection
         * @param src_id The ID of the source processor
         * @param src_pipe The number of the source pipe
         * @param dest_id The ID of the destination processor
         * @param dest_pipe The number of the destination pipe
         */
        public Connection(int src_id, int src_pipe, int dest_id, int dest_pipe)
        {
            super();
            m_sourceProcessorId = src_id;
            m_destinationProcessorId = dest_id;
            m_sourcePipeNumber = src_pipe;
            m_destinationPipeNumber = dest_pipe;
        }

        @Override
        public String toString()
        {
            return m_sourceProcessorId + "#" + m_sourcePipeNumber + "->"
                    + m_destinationProcessorId + "#" + m_destinationPipeNumber;
        }

        @Override
        public int hashCode()
        {
            return m_sourceProcessorId * m_sourcePipeNumber
                    + m_destinationProcessorId * m_destinationPipeNumber;
        }

        @Override
        public boolean equals(Object o)
        {
            if (o == null || !(o instanceof Connection))
            {
                return false;
            }
            Connection c = (Connection) o;
            return c.m_sourcePipeNumber == m_sourcePipeNumber
                    && c.m_destinationPipeNumber == m_destinationPipeNumber
                    && c.m_sourceProcessorId == m_sourceProcessorId
                    && c.m_destinationProcessorId == m_destinationProcessorId;
        }
    }


    protected static void checkForException(Processor p1, int i, Processor p2, int j)
    {
        try
        {
            Class<?> out_class = p1.getOutputType(i);
            if (s_checkForBounds && out_class == null)
            {
                // p1 has no output, so how would you connect it to p2?
                throw new IndexOutOfBoundsException(p1, i, p2, j);
            }
            if (out_class != null && out_class.equals(Variant.class))
            {
                // Skip type checking
                return;
            }
            /*@ non_null @*/ Set<Class<?>> in_classes = p2.getInputType(j);
            if (in_classes.isEmpty())
            {
                if (s_checkForBounds)
                {
                    // p2 has no input, so how would you connect it to p1?
                    throw new IndexOutOfBoundsException(p1, i, p2, j);
                }
                else
                {
                    // We don't mind that we connect an output to something
                    // that has no input
                    return;
                }
            }
            for (Class<?> in_class : in_classes)
            {
                if (in_class.equals(Variant.class) || in_class.isAssignableFrom(out_class)
                        || in_class.equals(Object.class))
                {
                    // Found a compatible in/out pair of types: return without exception
                    return;
                }
            }
            throw new IncompatibleTypesException(p1, i, p2, j);
        }
        catch (ArrayIndexOutOfBoundsException e)
        {
            throw new IndexOutOfBoundsException(p1, i, p2, j);
        }
    }

    public static Collection<Connection> getConnections(Processor p)
    {
        Set<Connection> connections = new HashSet<Connection>();
        int p_id = p.getId();
        for (int i = 0; i < p.getInputArity(); i++)
        {
            Pullable pl = p.getPullableInput(i);
            if (pl == null)
            {
                continue;
            }
            Processor d_p = pl.getProcessor();
            if (d_p == null)
            {
                continue;
            }
            connections.add(new Connection(d_p.getId(), pl.getPosition(), p_id, i));
        }
        for (int i = 0; i < p.getOutputArity(); i++)
        {
            Pushable ps = p.getPushableOutput(i);
            if (ps == null)
            {
                continue;
            }
            Processor d_p = ps.getProcessor();
            if (d_p == null)
            {
                continue;
            }
            connections.add(new Connection(p_id, i, d_p.getId(), ps.getPosition()));
        }
        return connections;
    }


}
